package me.yushuai.wukong.base.service.impl;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import me.yushuai.wukong.base.api.dto.CreatePackingDTO;
import me.yushuai.wukong.base.api.dto.*;
import me.yushuai.wukong.base.convertor.PackingConvertor;
import me.yushuai.wukong.base.entity.PackingBasic;
import me.yushuai.wukong.base.entity.PackingUom;
import me.yushuai.wukong.base.repository.PackingBasicRepository;
import me.yushuai.wukong.base.repository.PackingUomRepository;
import me.yushuai.wukong.base.service.PackingService;
import me.yushuai.wukong.common.dto.PageDTO;
import me.yushuai.wukong.common.exceptin.BizIdNotExistException;
import me.yushuai.wukong.common.exceptin.DuplicateBizIdException;
import me.yushuai.wukong.common.exceptin.NotExistException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.Predicate;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 包规业务实现类
 *
 * @author zhoushuai@189.cn
 * @since 2022-08-18
 */
@Service
public class PackingServiceImpl  implements PackingService {

    public static final int MAXIMUM_SIZE = 1000;

    public static final int CONCURRENCY_LEVEL = 7;

    public static final int DEFAULT_EXPIRE = 30;

    private final PackingConvertor packingConvertor;

    private final PackingBasicRepository packingBasicRepository;

    private final PackingUomRepository packingUomRepository;

    /**
     * 包规的本地缓存
     * TODO 本地缓存与ORM框架的二级缓存相比有什么不同的地方。
     * ORM框架的二级缓存只能针对实体对象做缓存，不能对实体之外的对象做缓交
     */
    private final Cache<Long, PackingBasic> cachePackingId = CacheBuilder.newBuilder()
            .maximumSize(MAXIMUM_SIZE)
            .expireAfterAccess(DEFAULT_EXPIRE, TimeUnit.MINUTES)
            .expireAfterWrite(DEFAULT_EXPIRE, TimeUnit.MINUTES)
            .concurrencyLevel(CONCURRENCY_LEVEL)
            .build();

    /**
     * 包规本地缓存
     */
    private final Cache<String, PackingBasic> cachePackingBizId = CacheBuilder.newBuilder()
            .maximumSize(MAXIMUM_SIZE)
            .expireAfterAccess(DEFAULT_EXPIRE, TimeUnit.MINUTES)
            .expireAfterWrite(DEFAULT_EXPIRE, TimeUnit.MINUTES)
            .concurrencyLevel(CONCURRENCY_LEVEL)
            .build();

    @Autowired
    public PackingServiceImpl(PackingConvertor packingConvertor, PackingBasicRepository packingBasicRepository,
                              PackingUomRepository packingUomRepository) {
        this.packingConvertor = packingConvertor;
        this.packingBasicRepository = packingBasicRepository;
        this.packingUomRepository = packingUomRepository;
    }

    @Override
    public Long createPacking(CreatePackingDTO dto) throws DuplicateBizIdException {

        //1. 检查包规代码是否存在
        if (this.packingBasicRepository.existsByBizId(dto.getBizId())) {
            throw new DuplicateBizIdException(dto.getBizId(), PackingBasic.class);
        }

        //2. 新增包规
        final PackingBasic packingBasic = this.packingConvertor.mapToPacking(dto);
        final PackingBasic savedPackingBasic = this.packingBasicRepository.save(packingBasic);

        //3. 新建包规的基本单位
        final Long packingId = savedPackingBasic.getPackingId();
        final PackingUomDTO source = dto.getBasicUom();
        //3.1 如果未填写基本单位直接跳过
        if (Objects.isNull(source)) {
            return packingId;
        }

        final PackingUomDTO target = this.packingConvertor.mapToUomDto(packingId, source);
        this.createPackingUom(target);

        //4. 新增包规基本单位
        final PackingUom packingUom = packingConvertor.mapToPackingUom(target);
        this.packingUomRepository.save(packingUom);

        //4. 返回新增成功的包规编号
        return packingBasic.getPackingId();
    }


    @Override
    public Long updatePackingById(UpdatePackingDTO dto) throws NotExistException {
        final Long packingId = dto.getPackingId();
        final PackingBasic packingBasic = getPackingElseThrow(packingId);

        //2. 通过包规编号更新包规信息
        this.packingConvertor.updateToPacking(dto, packingBasic);
        this.packingBasicRepository.save(packingBasic);

        //4. 返回包规的编号
        return dto.getPackingId();
    }

    @Override
    public void deletePacking(Long packingId) throws NotExistException {
        //1. 检查包规是否存在
        if (this.existsByPackingId(packingId)) {
            throw new NotExistException(packingId, PackingBasic.class);
        }

        //2. 删除包规记录
        this.packingBasicRepository.deleteById(packingId);
    }

    @Override
    public PackingDTO findPackingById(Long packingId) throws NotExistException {
        //1. 查询包规基本信息
        final PackingBasic packingBasic = getPackingElseThrow(packingId);

        //2. 查询包规单位列表
        final List<PackingUom> packingUomList = this.packingUomRepository.findByPackingId(packingId);

        //3. 返回查询结果
        return this.packingConvertor.mapToPackingDto(packingBasic, packingUomList);
    }

    @Override
    public PackingDTO findPackingByBizId(String packingBizId) throws BizIdNotExistException {
        //1. 使用业务Id查询包规的基本信息
        PackingBasic packingBasic = getPackingElseThrow(packingBizId);

        //2. 查询包规的单位列表
        final List<PackingUom> packingUomList = this.packingUomRepository.findByPackingId(packingBasic.getPackingId());

        //3. 返回包规的列表信息
        return this.packingConvertor.mapToPackingDto(packingBasic, packingUomList);
    }

    @Override
    public BigDecimal convertQtyToDefaultUom(Long packingUomId, Long packingId, BigDecimal qty) throws NotExistException {
        //1. first get PackingUom by packingUomId and packingId
        final PackingUom selectedQty = this.packingUomRepository.findByPackingIdAndUomId(packingId, packingUomId)
                .orElseThrow(() -> new NotExistException(packingUomId, PackingUom.class));

        //2. second using the selectedQtyUom convert to qty.
        return qty.multiply(selectedQty.getQty());
    }

    @Override
    public PageDTO<PackingDTO> pagingSearchPacking(PagingSearchPacking dto) {
        //1. 构建查询条件
        final Specification<PackingBasic> spec = (root, query, criteriaBuilder) -> {
            List<Predicate> list = new ArrayList<>();
            final String bizId = dto.getBizId();
            if (StringUtils.hasText(bizId)) {
                String likeBizId = bizId.trim() + "%";
                list.add(criteriaBuilder.like(root.get("bizId").as(String.class), likeBizId));
            }

            final List<String> bizIds = dto.getBizIds();
            if (Objects.nonNull(bizIds) && !bizIds.isEmpty()) {
                list.add(criteriaBuilder.in(root.get("bizId").as(String.class)).in(
                        bizIds.stream().filter(StringUtils::hasText).collect(Collectors.toList())
                ));
            }
            return criteriaBuilder.and(list.toArray(Predicate[]::new));
        };

        //2. 构建分页参数信息
        final int curPage = dto.getCurrent();
        final int pageSize = dto.getSize();
        final PageRequest page = PageRequest.of(curPage, pageSize);


        //3. 执行查询操作
        final Page<PackingBasic> packingList = this.packingBasicRepository.findAll(spec, page);

        //4. 新建返回对象并返回
        dto.setTotal(packingList.getTotalElements());
        return PageDTO.of(dto, this.packingConvertor.mapToDtoList(packingList.getContent()));
    }


    @Override
    public boolean existsByPackingId(Long packingId) {
        //1. first get packing form local cache
        final PackingBasic packingBasic = this.cachePackingId.getIfPresent(packingId);
        if (Objects.nonNull(packingBasic)) {
            return true;
        }

        //2. second checking the packing if exists form database.
        return this.packingBasicRepository.existsById(packingId);
    }


    @Override
    public Long createPackingUom(PackingUomDTO dto) throws DuplicateBizIdException {

        //1. 检查包规编码是否可用
        final Long packingId = dto.getPackingId();
        final String packingUomBizId = dto.getBizId();
        if (this.packingUomRepository.existsByPackingIdAndPackingUomBizId(packingId, packingUomBizId)) {
            throw new DuplicateBizIdException(packingUomBizId, PackingUom.class);
        }

        //3. 新增包规单位
        final PackingUom packingUom = packingConvertor.mapToPackingUom(dto);
        this.packingUomRepository.save(packingUom);
        return packingUom.getUomId();
    }

    @Override
    public Long updatePackingUomById(PackingUomDTO dto) throws NotExistException {

        //1. 检查包规单位是否存在
        final Long uomId = dto.getUomId();
        final PackingUom packingUom = this.packingUomRepository.findById(uomId)
                .orElseThrow(() -> new NotExistException(uomId, PackingUom.class));

        //2. 通过包规单位编号和包规编号更新包规信息

        //3. 更新包规单位信息
        this.packingConvertor.updateToPackingUom(dto, packingUom);
        this.packingUomRepository.save(packingUom);

        //4. 返回更新后包规单位编号
        return dto.getUomId();
    }

    @Override
    public void deletePackingUom(Long packingId, Long packingUomId) throws NotExistException {
        //1. 检查包规单位是否存在
        this.assertUomIdHasExist(packingId, packingUomId);

        //2. 删除包规单位记录
        this.packingUomRepository.deleteByPackingIdAndUomId(packingId, packingUomId);
    }

    @Override
    public PackingUomDTO findPackingUomById(Long packingId, Long packingUomId) throws NotExistException {
        //1. 通过packingId和uomId查询包规单位信息
        final PackingUom packingUom = this.packingUomRepository.findByPackingIdAndUomId(packingId, packingUomId)
                .orElseThrow(() -> new NotExistException(packingUomId, PackingUom.class));

        //2. 检查包规单位信息是否存在并抛出异常
        if (Objects.isNull(packingUom)) {
            throw new NotExistException(packingUomId, PackingUom.class);
        }

        //3. 返回查询结果
        return this.packingConvertor.mapToPackingUomDto(packingUom);
    }

    @Override
    public boolean existsByPackingUomId(Long packId, Long packingUomId) {
        return this.packingUomRepository.existsById(packId, packingUomId);
    }

    @Override
    public boolean isBasicUom(Long packingId, Long packingUomId) throws NotExistException {
        final PackingUom packingUom = this.packingUomRepository.findByPackingIdAndUomId(packingId, packingUomId)
                .orElseThrow(() -> new NotExistException(packingUomId, PackingUom.class));

        return Objects.nonNull(packingUom) && packingUom.getBasicUnit();
    }


    //-------------------------------------private method --------------------------------------------

    /**
     * get packing by packingId
     *
     * @param packingId id of Packing
     * @return return the Packing form packingId
     * @throws NotExistException if the packingId not exist throw exception
     */
    private PackingBasic getPackingElseThrow(Long packingId) throws NotExistException {
        //1. first get Packing form local cache, if the packing exists and return it.
        PackingBasic packingBasic = this.cachePackingId.getIfPresent(packingId);
        if (Objects.nonNull(packingBasic)) {
            return packingBasic;
        }

        //2. second get Packing form databases, if the packingId not exist and throw NotExistException
        final Optional<PackingBasic> packingOptional = this.packingBasicRepository.findById(packingId);
        packingBasic = packingOptional.orElseThrow(() -> new NotExistException(packingId, PackingBasic.class));

        //3. third put the packing to cache and return it.
        this.cachePackingId.put(packingId, packingBasic);
        this.cachePackingBizId.put(packingBasic.getBizId(), packingBasic);
        return packingBasic;
    }

    /**
     * get Packing by bizId, if the bizId not exists throw exception {@link BizIdNotExistException}
     *
     * @param bizId business id
     * @return return the Packing form bizId
     * @throws BizIdNotExistException if the bizId not exists than throw the exception
     */
    private PackingBasic getPackingElseThrow(String bizId) throws BizIdNotExistException {
        //1. first get Packing from local cache
        PackingBasic packingBasic = this.cachePackingBizId.getIfPresent(bizId);
        if (Objects.nonNull(packingBasic)) {
            return packingBasic;
        }

        //2. second get Packing from databases
        packingBasic = this.packingBasicRepository.findByBizId(bizId)
                .orElseThrow(() -> new BizIdNotExistException(bizId, PackingBasic.class));

        //3. third put the packing to cache and return it.
        this.cachePackingId.put(packingBasic.getPackingId(), packingBasic);
        this.cachePackingBizId.put(packingBasic.getBizId(), packingBasic);
        return packingBasic;
    }

    /**
     * 断言包规编码不存在，如果包规编码存在抛出异常
     *
     * @param bizId 业务编号
     * @throws DuplicateBizIdException 检查业务Id是否已存在
     */
    private void assertNotExistsBizId(String bizId) throws DuplicateBizIdException {
        //1. first checking the packingBizId if exists from local cache.
        final PackingBasic packingBasic = this.cachePackingBizId.getIfPresent(bizId);
        if (Objects.nonNull(packingBasic)) {
            throw new DuplicateBizIdException(bizId, PackingBasic.class);
        }

        //2. checking from database
        if (this.packingBasicRepository.existsByBizId(bizId)) {
            throw new DuplicateBizIdException(bizId, PackingBasic.class);
        }
    }

    /**
     * 断言包规编号已存在，如果包规编号不存在抛出异常
     *
     * @param packingId 包装编号
     * @throws NotExistException 当包装不存在时抛出异常
     */
    private void assertPackingIdExist(Long packingId) throws NotExistException {
        if (!this.existsByPackingId(packingId)) {
            throw new NotExistException(packingId, PackingBasic.class);
        }
    }

    private void assertUomIdHasExist(Long packingId, Long uomId) throws NotExistException {
        if (this.packingUomRepository.existsById(packingId, uomId)) {
            throw new NotExistException(uomId, PackingUom.class);
        }
    }

    private void assertPackingIdAndUomCodeNotExist(Long packingId, String uomBizId) throws DuplicateBizIdException {
        if (this.packingUomRepository.existsByPackingIdAndPackingUomBizId(packingId, uomBizId)) {
            throw new DuplicateBizIdException(uomBizId, PackingUom.class);
        }
    }

}




